// Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
// Copyright (C) 2010  Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#include <nel/misc/types_nl.h>
#include <nel/misc/common.h>
#include <nel/misc/variable.h>
#include <nel/misc/command.h>
#include <nel/misc/file.h>
#include <nel/misc/path.h>

#include "game_share/utils.h"

#include "pd_lib/pd_lib.h"
#include "pd_lib/db_description_parser.h"
#include "pd_lib/pd_messages.h"

#include "log_analyser_service.h"

using namespace std;
using namespace NLMISC;
using namespace NLNET;
using namespace RY_PDS;

/*
 * SUMMARY OF COMMANDS:
 *
 * displayLogContent <description file> <filename>
 *   Display readable content of a log file
 *   description file: xml file of database description (with full path), 
 *      generated by pd_parser, this must be the matching description file
 *      to the log file to display (that is the youngest description that
 *      is older than the log file)
 *   filename: the file to display content of (with full path)
 *
 */


std::string	decodeDbPathId(const std::string& pathid)
{
	std::string	shard("");
	uint		id;

	string::size_type		p = pathid.find(':');

	if (p == std::string::npos)
	{
		NLMISC::fromString(pathid, id);
	}
	else
	{
		shard = pathid.substr(0, p);
		NLMISC::fromString(pathid.substr(p+1), id);
	}

	return CPDSLib::getLogDirectory(id, shard);
}


class CSimpleFileDisplayer : public IDisplayer
{
public:

	CSimpleFileDisplayer(const CSString& fileName): _FileName(fileName)
	{
		_File=NULL;
		_TmpFileName= _FileName+ ".tmp";

		// if the file that we've been asked to create already exists then try to delete the existing file
		if (NLMISC::CFile::fileExists(_FileName))
			NLMISC::CFile::deleteFile(_FileName);
		DROP_IF(NLMISC::CFile::fileExists(_FileName),"Failed to delete file: "+_FileName,return);

		// if the temp file that we're going to use already exists then try to delete the existing file
		if (NLMISC::CFile::fileExists(_TmpFileName))
			NLMISC::CFile::deleteFile(_TmpFileName);
		DROP_IF(NLMISC::CFile::fileExists(_TmpFileName),"Failed to delete file: "+_TmpFileName,return);

		_File= fopen(_TmpFileName.c_str(),"wb");
	}
	
	~CSimpleFileDisplayer()
	{
		fclose(_File);

		// rename the created file...
		DROP_IF(!NLMISC::CFile::fileExists(_TmpFileName),"No output file created: "+_TmpFileName,return);
		DROP_IF(NLMISC::CFile::fileExists(_FileName),"Cannot rename output file '"+_TmpFileName+"' because another file is in the way: "+_FileName,return);
		NLMISC::CFile::moveFile(_FileName.c_str(),_TmpFileName.c_str());
		DROP_IF(!NLMISC::CFile::fileExists(_FileName),"Failed to create final output file: '"+_FileName+"' from tmp file: '"+_TmpFileName+"'",return);
	}

	bool isOK() const
	{
		return _File!= NULL;
	}

protected:

	virtual void doDisplay( const CLog::TDisplayInfo& args, const char *message)
	{
		if (isOK())
		{
			fwrite(message,strlen(message),1,_File);
		}
	}

private:
	FILE* _File;
	CSString _FileName;
	CSString _TmpFileName;

};

NLMISC_CATEGORISED_COMMAND(pd_log, executeToFile, "execute a command, spuing output to a file ", "<filename> <commandline>")
{
	// split the raw commandline into a file name and remaining commandline
	NLMISC::CSString s= rawCommandString;
	// skip the wrapper command name
	s.strtok(" \t");
	// get the file name
	NLMISC::CSString fileName=s.strtok(" \t",true).unquoteIfQuoted();

	// cleanup the remaining commandline
	s=s.strip();
	DROP_IF(s.empty(),"Too few elements found in command line",return false);

	// setup a log object with an attached file displayer
	CSimpleFileDisplayer* displayer= new CSimpleFileDisplayer(fileName);
	DROP_IF(!displayer->isOK(),"Aborting command execution because failed to setup file displayer",return false);
	CLog theLog;
	theLog.addDisplayer(displayer,true);

	// execute the command...
	NLMISC::ICommand::execute(s, theLog);

	// remove the displayer and destroy it to ensure files are closed etc
	theLog.removeDisplayer(displayer);
	delete displayer;

	// success fo return true
	return true;
}

//
NLMISC_CATEGORISED_COMMAND(pd_log, displayLogFile, "display pd_log file human readable content", "<description file> <filename>")
{
	if (args.size() != 2)
		return false;

	string	descriptionfile = args[0];
	string	filename = args[1];

	CDBDescriptionParser	desc;

	if (!desc.loadDescriptionFile(descriptionfile))
	{
		log.displayNL("#! Failed to load database description file '%s'", descriptionfile.c_str());
		return false;
	}

	if (!desc.buildColumns())
	{
		log.displayNL("#! Failed to build database columns for description '%s'", descriptionfile.c_str());
		return false;
	}

	CIFile					ifile;
	std::vector<CUpdateLog>	updateLog;

	if (!ifile.open(filename))
	{
		log.displayNL("#! Failed to open file '%s'", filename.c_str());
		return false;
	}

	while (!ifile.eof())
	{
		try
		{
			ifile.serialCont(updateLog);
		}
		catch (const Exception& e)
		{
			log.displayNL("#! Failed to load file '%s': %s", filename.c_str(), e.what());
			return false;
		}

		uint	i;
		for (i=0; i<updateLog.size(); ++i)
			updateLog[i].display(desc, log);
	}

	return true;
}

//
NLMISC_CATEGORISED_COMMAND(pd_log, displayLog, "display pd_log file human readable content", "[shard:]<databaseId> <filename w/o path>")
{
	if (args.size() != 2)
		return false;
/*
	uint	databaseId;
	NLMISC::fromString(args[0], databaseId);
	string	logpath = CPDSLib::getLogDirectory(databaseId);
*/
	string	logpath = decodeDbPathId(args[0]);

	string	filename = args[1];

	std::string	descriptionfile = CUpdateLog::electDescription(logpath+filename);

	if (descriptionfile.empty())
	{
		log.displayNL("#! Failed to elect description file for file '%s'", filename.c_str());
		return false;
	}

	CDBDescriptionParser	desc;

	if (!desc.loadDescriptionFile(descriptionfile))
	{
		log.displayNL("#! Failed to load database description file '%s'", descriptionfile.c_str());
		return false;
	}

	if (!desc.buildColumns())
	{
		log.displayNL("#! Failed to build database columns for description '%s'", descriptionfile.c_str());
		return false;
	}

	CIFile					ifile;
	std::vector<CUpdateLog>	updateLog;

	if (!ifile.open(logpath+filename))
	{
		log.displayNL("#! Failed to open file '%s'", filename.c_str());
		return false;
	}

	while (!ifile.eof())
	{
		try
		{
			ifile.serialCont(updateLog);
		}
		catch (const Exception& e)
		{
			log.displayNL("#! Failed to load file '%s': %s", filename.c_str(), e.what());
			return false;
		}

		uint	i;
		for (i=0; i<updateLog.size(); ++i)
			updateLog[i].display(desc, log);
	}

	return true;
}

//
NLMISC_CATEGORISED_COMMAND(pd_log, displayLogs,
						   "display logs content between 2 dates (date format is YYYY.MM.DD.hh.mm[.ss], or <-|+>nn<d|h|m|s> to specify relative date)",
						   "[shard:]<databaseId> <startdate> [enddate]")
{
	if (args.size() < 2 || args.size() > 3)
		return false;

/*
	uint	databaseId;
	NLMISC::fromString(args[0], databaseId);
	string	logpath = CPDSLib::getLogDirectory(databaseId);
*/
	string	logpath = decodeDbPathId(args[0]);

	CTimestamp	startdate;
	CTimestamp	enddate;

	startdate.setToCurrent();
	startdate.fromString(args[1].c_str());

	if (args.size() == 3)
	{
		enddate = startdate;
		enddate.fromString(args[2].c_str());
	}
	else
	{
		enddate.setToCurrent();
	}

	CLogAnalyserService::CQuery*	q = CLogAnalyserService::getInstance()->getCurrentQuery();
	CUpdateLog::displayLogs(logpath, startdate, enddate, log, (q != NULL ? (float*)&(q->Progress) : NULL));

	return true;
}

//
NLMISC_CATEGORISED_COMMAND(pd_log, searchEId,
						   "search for references to an entity id in logs between 2 dates (date format is YYYY:MM:DD:hh:mm[:ss], or <-|+>nn<d|h|m|s> to specify relative date)",
						   "[shard:]<databaseId> <entityId> <startdate> [enddate]")
{
	if (args.size() < 3 || args.size() > 4)
		return false;

/*
	uint	databaseId;
	NLMISC::fromString(args[0], databaseId);
	string	logpath = CPDSLib::getLogDirectory(databaseId);
*/
	string	logpath = decodeDbPathId(args[0]);

	CEntityId	entityId;
	entityId.fromString(args[1].c_str());

	CTimestamp	startdate;
	CTimestamp	enddate;

	startdate.setToCurrent();
	startdate.fromString(args[2].c_str());

	if (args.size() == 4)
	{
		enddate = startdate;
		enddate.fromString(args[3].c_str());
	}
	else
	{
		enddate.setToCurrent();
	}

	CLogAnalyserService::CQuery*	q = CLogAnalyserService::getInstance()->getCurrentQuery();
	CUpdateLog::displayLogs(logpath, entityId, startdate, enddate, log, (q != NULL ? (float*)&(q->Progress) : NULL));

	return true;
}

//
NLMISC_CATEGORISED_COMMAND(pd_log, searchString,
						   "search for references to a string in logs between 2 dates (date format is YYYY:MM:DD:hh:mm[:ss], or <-|+>nn<d|h|m|s> to specify relative date)",
						   "[shard:]<databaseId> <string> <startdate> [enddate]")
{
	if (args.size() < 3 || args.size() > 4)
		return false;

/*
	uint	databaseId;
	NLMISC::fromString(args[0], databaseId);
	string	logpath = CPDSLib::getLogDirectory(databaseId);
*/
	string	logpath = decodeDbPathId(args[0]);

	std::string	str = args[1];

	CTimestamp	startdate;
	CTimestamp	enddate;

	startdate.setToCurrent();
	startdate.fromString(args[2].c_str());

	if (args.size() == 4)
	{
		enddate = startdate;
		enddate.fromString(args[3].c_str());
	}
	else
	{
		enddate.setToCurrent();
	}

	CLogAnalyserService::CQuery*	q = CLogAnalyserService::getInstance()->getCurrentQuery();
	CUpdateLog::displayLogs(logpath, str, startdate, enddate, log, (q != NULL ? (float*)&(q->Progress) : NULL));

	return true;
}

//
NLMISC_CATEGORISED_COMMAND(pd_log, searchEIds,
						   "search for references to multiple entity ids in logs between 2 dates (date format is YYYY:MM:DD:hh:mm[:ss], or <-|+>nn<d|h|m|s> to specify relative date)",
						   "[shard:]<databaseId> [entityId+] - <startdate> [enddate]")
{
	if (args.size() < 4)
		return false;

/*
	uint	databaseId;
	NLMISC::fromString(args[0], databaseId);
	string	logpath = CPDSLib::getLogDirectory(databaseId);
*/
	string	logpath = decodeDbPathId(args[0]);

	std::vector<NLMISC::CEntityId>	ids;
	uint	i = 1;
	while (i < args.size() && args[i] != "-")
	{
		NLMISC::CEntityId	id;
		id.fromString(args[i].c_str());
		ids.push_back(id);
		++i;
	}

	++i;

	if (i >= args.size())
		return false;

	CTimestamp	startdate;
	CTimestamp	enddate;

	startdate.setToCurrent();
	startdate.fromString(args[i].c_str());

	++i;

	if (i < args.size())
	{
		enddate = startdate;
		enddate.fromString(args[i].c_str());
	}
	else
	{
		enddate.setToCurrent();
	}

	CLogAnalyserService::CQuery*	q = CLogAnalyserService::getInstance()->getCurrentQuery();
	CUpdateLog::displayLogs(logpath, ids, startdate, enddate, log, (q != NULL ? (float*)&(q->Progress) : NULL));

	return true;
}

//
NLMISC_CATEGORISED_COMMAND(pd_log, searchValueByEId,
						   "search for references to an entity id in logs between 2 dates (date format is YYYY:MM:DD:hh:mm[:ss], or <-|+>nn<d|h|m|s> to specify relative date)",
						   "[shard:]<databaseId> <entityId> <valuePath> <startdate> [enddate]")
{
	if (args.size() < 4 || args.size() > 5)
		return false;

/*
	uint	databaseId;
	NLMISC::fromString(args[0], databaseId);
	string	logpath = CPDSLib::getLogDirectory(databaseId);
*/
	string	logpath = decodeDbPathId(args[0]);

	CEntityId	entityId;
	entityId.fromString(args[1].c_str());

	std::string	valuePath = args[2];

	CTimestamp	startdate;
	CTimestamp	enddate;

	startdate.setToCurrent();
	startdate.fromString(args[2].c_str());

	if (args.size() == 5)
	{
		enddate = startdate;
		enddate.fromString(args[3].c_str());
	}
	else
	{
		enddate.setToCurrent();
	}

	CLogAnalyserService::CQuery*	q = CLogAnalyserService::getInstance()->getCurrentQuery();
	CUpdateLog::displayLogs(logpath, entityId, valuePath, startdate, enddate, log, (q != NULL ? (float*)&(q->Progress) : NULL));

	return true;
}



//
NLMISC_CATEGORISED_COMMAND(pd_log, displayDescription,
						   "Display database description. Asks for a specific file if more than one found.",
						   "[shard:]<databaseId> [descriptionfile]")
{
	if (args.size() < 1 || args.size() > 2)
		return false;

/*
	uint	databaseId;
	NLMISC::fromString(args[0], databaseId);
	string	logpath = CPDSLib::getLogDirectory(databaseId);
*/
	string	logpath = decodeDbPathId(args[0]);

	std::string	useFile;

	if (args.size() == 1)
	{
		std::vector<std::string>	files, found;
		NLMISC::CPath::getPathContent(logpath, false, false, true, files);

		std::sort(files.begin(), files.end());

		uint	i;
		for (i=0; i<files.size(); ++i)
			if (NLMISC::CFile::getExtension(files[i]) == "description")
				found.push_back(files[i]);

		if (found.size() == 0)
		{
			log.displayNL("## no description file found in path '%s'", logpath.c_str());
			return true;
		}

		if (found.size() > 1)
		{
			log.displayNL("## found multiple description files in path '%s':", logpath.c_str());
			for (i=0; i<found.size(); ++i)
				log.displayNL("#- %s", NLMISC::CFile::getFilename(found[i]).c_str());
			log.displayNL("## found %d files, please select one", found.size());
			return true;
		}

		useFile = NLMISC::CFile::getFilename(found[0]);

		log.displayNL("## Using file '%s'", useFile.c_str());
	}
	else
	{
		useFile = args[1];
	}

	CDBDescriptionParser	desc;
	if (!desc.loadDescriptionFile(logpath + useFile))
		return false;

	desc.buildColumns();
	desc.display(log);

	return true;
}


//
NLMISC_CATEGORISED_COMMAND(pd_log, displayTableDescription,
						   "Display table description. Asks for a specific file if more than one found.",
						   "[shard:]<databaseId> <tableName> [descriptionfile]")
{
	if (args.size() < 2 || args.size() > 3)
		return false;

/*
	uint	databaseId;
	NLMISC::fromString(args[0], databaseId);
	string	logpath = CPDSLib::getLogDirectory(databaseId);
*/
	string	logpath = decodeDbPathId(args[0]);

	std::string	useFile;

	if (args.size() == 2)
	{
		std::vector<std::string>	files, found;
		NLMISC::CPath::getPathContent(logpath, false, false, true, files);

		std::sort(files.begin(), files.end());

		uint	i;
		for (i=0; i<files.size(); ++i)
			if (NLMISC::CFile::getExtension(files[i]) == "description")
				found.push_back(files[i]);

		if (found.size() == 0)
		{
			log.displayNL("## no description file found in path '%s'", logpath.c_str());
			return true;
		}

		if (found.size() > 1)
		{
			log.displayNL("## found multiple description files in path '%s':", logpath.c_str());
			for (i=0; i<found.size(); ++i)
				log.displayNL("#- %s", NLMISC::CFile::getFilename(found[i]).c_str());
			log.displayNL("## found %d files, please select one", found.size());
			return true;
		}

		useFile = NLMISC::CFile::getFilename(found[0]);

		log.displayNL("## Using file '%s'", useFile.c_str());
	}
	else
	{
		useFile = logpath + args[2];
	}

	std::string	tableName = toLower(args[1]);

	CDBDescriptionParser	desc;
	if (!desc.loadDescriptionFile(logpath + useFile))
		return false;

	const CDatabaseNode&	db = desc.getDatabaseNode();

	uint	i;
	for (i=0; i<db.Tables.size(); ++i)
		if (toLower(db.Tables[i].Name) == tableName)
			break;

	if (i == db.Tables.size())
		return false;

	desc.buildColumns();
	desc.displayTable(i, log);

	return true;
}


